package xyz.lwm.lazycat.utility;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * It
 *
 * @author lwm
 * @param <E>
 */
public final class It<E> {

    private static final It<?> NONE = new It<>();

    private final E value;

    private It() {
        this.value = null;
    }

    private It(E value) {
        this.value = Objects.requireNonNull(value);
    }

    public static <T> It<T> of(@Nullable T value) {
        return value != null ? new It<>(value) : none();
    }

    @SuppressWarnings("unchecked")
    public static <T> It<T> none() {
        return (It<T>) NONE;
    }

    /**
     * @return true if value is not null
     */
    public boolean isPresent() {
        return value != null;
    }

    /**
     * filter value
     *
     * @param p predicate
     * @return It
     */
    public It<E> filter(@NotNull Predicate<? super E> p) {
        Assert.notNull(p, "predicate");
        if (!isPresent()) {
            return none();
        }
        return p.test(value) ? this : none();
    }

    /**
     * let and map to new value
     *
     * @param f value mapper
     * @return It
     * @param <U> new value type
     */
    public <U> It<U> let(@NotNull Function<? super E, ? extends U> f) {
        Assert.notNull(f, "function");
        if (!isPresent()) {
            return none();
        }
        return of(f.apply(value));
    }

    /**
     * or else other value
     *
     * @param other other it
     * @return It
     */
    public It<E> orElse(@NotNull It<E> other) {
        return isPresent() ? this : other;
    }

    /**
     * or else other value
     *
     * @param other value
     * @return It
     */
    public It<E> orElse(E other) {
        return orElse(It.of(other));
    }

    /**
     * or else other value
     *
     * @param s value supplier
     * @return It
     */
    public It<E> orElse(@NotNull Supplier<? extends E> s) {
        Assert.notNull(s, "supplier");
        return orElse(s.get());
    }

    /**
     * handle value
     *
     * @param c consumer
     * @return It
     */
    public It<E> also(@NotNull Consumer<? super E> c) {
        Assert.notNull(c, "consumer");
        if (isPresent()) {
            c.accept(value);
        }
        return this;
    }

    /**
     * get value or throw exception
     *
     * @param es throwable supplier
     * @return get value or throw exception
     * @param <X> exception type
     * @throws X exception
     */
    public <X extends Throwable> E getOrThrow(@NotNull Supplier<? extends X> es) throws X {
        if (isPresent()) {
            return value;
        }
        throw es.get();
    }

    /**
     * get value or throw {@link IllegalStateException}
     */
    public E get() {
        return getOrThrow(() -> new IllegalStateException("No value present"));
    }

    /**
     * get value or other value
     *
     * @param other value
     * @return value or other value
     */
    public E getOr(E other) {
        return isPresent() ? value : other;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }

        if (!(obj instanceof It)) {
            return false;
        }

        It<?> other = (It<?>) obj;
        return Objects.equals(value, other.value);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(value);
    }

    @Override
    public String toString() {
        return value != null
                ? String.format("It[%s]", value)
                : "It.null";
    }

}
